﻿Imports Microsoft.VisualBasic

'Developed by Robert R Freeman.  http://rrfreeman.blogspot.com

Public Class HtmlFilter
    Inherits System.IO.Stream

    Private Base As System.IO.Stream
    Private BufferCache As New Text.StringBuilder
    Private _SearchReplaceNVC As Specialized.NameValueCollection
    Private _SearchKeyDoubleMaxLength As Int64 = 0
    Private _CacheStartPosition As Int64 = 0

    Const HalfInt64 As Int64 = (Int64.MaxValue - 1) \ 2

    ''' <summary>
    ''' Set to true to bypass this filter on asynchronous (UpdatePanel) callbacks. Default = False.
    ''' </summary>
    Public BypassAsync As Boolean = False

    ''' <summary>
    ''' Set to the Maximum Length of a full Regex match.
    ''' </summary>
    Public SearchKeyMaxLength As Int64 = 0

    ''' <param name="responseStream">Provide the Response.Filter stream.</param>
    ''' <param name="SearchReplaceNVC">A key/value pair NameValueCollection containing the RegEx to search for and the Replacement Expression (Regex/ReplacementEx).  The regex must match if matching the match alone in order to successfully replace. Be careful using positive lookaheads/behinds that look outside the match.</param>
    ''' <param name="MaxMatchLength">Max estimated expression length.</param>
    ''' <param name="BypassAsync">Set to true to bypass this filter on asynchronous (UpdatePanel) callbacks. Default = False.</param>
    <ComponentModel.Description("Pass in the Response Stream, a key/value NameValuecollection (Regex/ReplacementEx), the max estimated expression length, and optionally bypass the filter on asynchronous callbacks.")> _
    Public Sub New(ByVal responseStream As IO.Stream, ByVal SearchReplaceNVC As Specialized.NameValueCollection, Optional ByVal MaxMatchLength As Int64 = HalfInt64, Optional ByVal BypassAsync As Boolean = False)
        If responseStream Is Nothing Then Throw New ArgumentNullException("ResponseStream")
        Me.BypassAsync = BypassAsync
        Me.SearchKeyMaxLength = MaxMatchLength
        If MaxMatchLength > HalfInt64 Then
            Throw New Exception("MaxMatchLength cannot be larger than " & HalfInt64.ToString)
        End If
        Me._SearchKeyDoubleMaxLength = MaxMatchLength * 2
        Me.SearchReplaceNVC = SearchReplaceNVC
        Me.Base = responseStream
    End Sub

    ''' <param name="PageToFilter">Provide the current Page object whose Response.Filter to set.</param>
    ''' <param name="SearchReplaceNVC">A key/value pair NameValueCollection containing the RegEx to search for and the Replacement Expression (Regex/ReplacementEx).  The regex must match if matching the match alone in order to successfully replace. Be careful using positive lookaheads/behinds that look outside the match.</param>
    ''' <param name="MaxMatchLength">Max estimated expression length.</param>
    ''' <param name="BypassAsync">Set to true to bypass this filter on asynchronous (UpdatePanel) callbacks. Default = False.</param>
    <ComponentModel.Description("Sets the Response.Filter checking for existing instances. Similar parameters to NEW.")> _
    Public Shared Sub SetHtmlFilter(ByVal PageToFilter As Page, ByVal SearchReplaceNVC As Specialized.NameValueCollection, Optional ByVal MaxMatchLength As Int64 = HalfInt64, Optional ByVal BypassAsync As Boolean = False)
        'If Page already has an HtmlFilter, then append to it, otherwise, create new.
        If PageToFilter.Response.Filter Is Nothing OrElse Not TypeOf PageToFilter.Response.Filter Is HtmlFilter Then
            PageToFilter.Response.Filter = New HtmlFilter(PageToFilter.Response.Filter, SearchReplaceNVC, MaxMatchLength, BypassAsync)
        Else
            Dim ExistingHtmlFilter As HtmlFilter = PageToFilter.Response.Filter
            'Add to the NVC
            ExistingHtmlFilter.SearchReplaceNVC.Add(SearchReplaceNVC)
            'Use the largest MaxLength
            ExistingHtmlFilter.SearchKeyMaxLength = Math.Max(MaxMatchLength, ExistingHtmlFilter.SearchKeyMaxLength)
            'Only Bypass Async if all instances have BypassAsync=true
            If ExistingHtmlFilter.BypassAsync Then ExistingHtmlFilter.BypassAsync = BypassAsync
        End If
    End Sub

    ''' <summary>
    ''' A key/value pair NameValueCollection containing the RegEx to search for and the Replacement Text (Regex/ReplacementText).
    ''' </summary>
    <ComponentModel.Description("A key/value pair NameValueCollection containing the RegEx to search for and the Replacement Text (Regex/ReplacementText).")> _
    Public Property SearchReplaceNVC() As Specialized.NameValueCollection
        Get
            Return _SearchReplaceNVC
        End Get
        Set(ByVal value As Specialized.NameValueCollection)
            _SearchReplaceNVC = value
        End Set
    End Property

    Public Overrides ReadOnly Property CanRead() As Boolean
        Get
            Return Me.Base.CanRead
        End Get
    End Property

    Public Overrides ReadOnly Property CanSeek() As Boolean
        Get
            Return Me.Base.CanSeek
        End Get
    End Property

    Public Overrides ReadOnly Property CanWrite() As Boolean
        Get
            Return Me.Base.CanWrite
        End Get
    End Property

    'On Flush, Write the rest of buffer.
    Public Overrides Sub Flush()
        If Me.BufferCache.Length > 0 Then
            flushing = True
            Me.Write(Nothing, 0, 0)
            flushing = False
        End If
    End Sub

    Public Overrides ReadOnly Property Length() As Long
        Get
            Return Me.Base.Length
        End Get
    End Property

    Public Overrides Property Position() As Long
        Get
            Return Me.Base.Position
        End Get
        Set(ByVal value As Long)
            Me.Base.Position = value
        End Set
    End Property

    Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
        Return Me.Base.Read(buffer, offset, count)
    End Function

    Public Overrides Function Seek(ByVal offset As Long, ByVal origin As System.IO.SeekOrigin) As Long
        Return Me.Base.Seek(offset, origin)
    End Function

    Public Overrides Sub SetLength(ByVal value As Long)
        Me.Base.SetLength(value)
    End Sub

    Private isFirstWrite As Boolean = True
    Private isAsync As Boolean = False
    Private flushing As Boolean = False
    Private adjustContentCount As Boolean = False
    Private ContentCount As Int64 = 0
    Private ContentCountStringLength As Integer = 0
    Private AsyncFirstIndex As Int64 = 0
    Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
        Dim HTML As String = Nothing

        'Put HTML in BufferCache.  buffer will be nothing on a flush.
        If Not (isAsync And BypassAsync) AndAlso buffer IsNot Nothing Then
            ' Get HTML code   
            HTML = System.Text.Encoding.UTF8.GetString(buffer, offset, count)
            Me.BufferCache.Append(HTML)
        End If

        'Check for an asynchronous write on first write attempt.
        If Not (isAsync And BypassAsync) AndAlso isFirstWrite Then
            Dim asyncRegEx As New Regex("^(\d+)\|[^\|]*\|[^\|]*\|", RegexOptions.Singleline)
            Dim asyncMatch As RegularExpressions.Match = asyncRegEx.Match(HTML)
            If asyncMatch.Success Then
                isAsync = True
                ContentCountStringLength = asyncMatch.Value.IndexOf("|")
                ContentCount = asyncMatch.Value.Substring(0, ContentCountStringLength)
                If HTML.Substring(asyncMatch.Length, 8) = """ OType=" Then
                    'SharePoint 2007 Strange Pipe delimited Group By ajax starts after content count characters
                    'and only supplies count for the EventValidation string.  The other 2 pipes are added before " OType=
                    AsyncFirstIndex = ContentCountStringLength + 1 + ContentCount
                Else
                    'Normally, ajax string starts after third pipe.
                    AsyncFirstIndex = asyncMatch.Length
                    adjustContentCount = True
                End If
            End If
            isFirstWrite = False
        End If

        'If asynchronous and BypassAsync is true, then bypass filter for use with Update Panels.
        If isAsync And BypassAsync Then
            If buffer IsNot Nothing Then
                Me.Base.Write(buffer, offset, count)
            End If
        ElseIf isAsync AndAlso Not Me.flushing Then
            'Do nothing until Flushing 
            'We have to cache entire output on Async in order to change the length value at beginning
        Else
            'If the Buffer cache is longer than twice the Search Max Length or is End or flushing
            If Me.BufferCache.Length >= Me._SearchKeyDoubleMaxLength OrElse flushing Then
                'Then process characters up to the MinFinalStrLength
                Dim MinFinalStrLength As Int64 = Me.SearchKeyMaxLength
                Dim bufferCacheStr As String = Me.BufferCache.ToString
                'Skip Async headers
                Dim asyncCacheStr = Nothing
                If isAsync Then
                    asyncCacheStr = bufferCacheStr.Substring(0, AsyncFirstIndex)
                    bufferCacheStr = bufferCacheStr.Substring(AsyncFirstIndex)
                End If
                Dim originalBufferCacheStrLength = bufferCacheStr.Length

                'Search and Replace RegEx on entire cache for each SearchReplaceNVC pair
                For i = 0 To Me.SearchReplaceNVC.Count - 1
                    Dim re As New Regex(Me.SearchReplaceNVC.Keys(i))
                    'Process only if there is a match
                    Dim mc As MatchCollection 'Use MatchCollection to find mcLastIndex for buffer length
                    mc = re.Matches(bufferCacheStr)
                    If mc IsNot Nothing AndAlso mc.Count > 0 Then
                        'Do Replacement
                        bufferCacheStr = Regex.Replace(bufferCacheStr, Me.SearchReplaceNVC.Keys(i), Me.SearchReplaceNVC(i))

                        'Calculate Minimum Final String length based on the last replacement index
                        'We will only cache after the last replacement, if replacements overlap, this could cause one to fail
                        ' however this is better than the alternative of duplicating replacements.
                        Dim mcLastIndex As Int64 = mc(mc.Count - 1).Index + mc(mc.Count - 1).Length
                        MinFinalStrLength = Math.Min(Me.BufferCache.Length - mcLastIndex, MinFinalStrLength)

                        'Update Content Count adding/subtracting replacement difference
                        If adjustContentCount Then ContentCount += bufferCacheStr.Length - originalBufferCacheStrLength
                    End If
                Next

                'If flushing, output entire cache.
                If flushing Then
                    If isAsync Then
                        'Restore Async Headers
                        BufferCache.Length = 0
                        If adjustContentCount Then
                            BufferCache.Append(ContentCount.ToString)
                            BufferCache.Append(asyncCacheStr.Substring(ContentCountStringLength))
                        Else
                            BufferCache.Append(asyncCacheStr)
                        End If
                        BufferCache.Append(bufferCacheStr)
                        bufferCacheStr = BufferCache.ToString
                    End If
                    If bufferCacheStr.Length > 0 Then
                        buffer = System.Text.Encoding.UTF8.GetBytes(bufferCacheStr)
                        Me.Base.Write(buffer, 0, buffer.Length)
                    End If
                Else
                    'Write all characters up to the MinFinalStrLength. Cache the rest.
                    Me.BufferCache.Length = 0
                    Me.BufferCache.Append(bufferCacheStr.Substring(bufferCacheStr.Length - MinFinalStrLength))
                    bufferCacheStr = bufferCacheStr.Substring(0, bufferCacheStr.Length - MinFinalStrLength)
                    If bufferCacheStr.Length > 0 Then
                        buffer = System.Text.Encoding.UTF8.GetBytes(bufferCacheStr)
                        Me.Base.Write(buffer, 0, buffer.Length)
                    End If
                End If
            End If

        End If

    End Sub

End Class
